package ai.timefold.solver.core.impl.neighborhood.maybeapi.move;

import java.util.Objects;
import java.util.function.Function;

import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStream;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStreamFactory;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.EnumeratingJoiners;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList;
import ai.timefold.solver.core.preview.api.move.Move;
import ai.timefold.solver.core.preview.api.move.SolutionView;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class ListSwapMoveDefinition<Solution_, Entity_, Value_>
        implements MoveDefinition<Solution_> {

    private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
    private final @Nullable Function<Entity_, Comparable> planningIdGetter;

    public ListSwapMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
        this.variableMetaModel = Objects.requireNonNull(variableMetaModel);
        this.planningIdGetter = SwapMoveDefinition.getPlanningIdGetter(variableMetaModel.entity());
    }

    @Override
    public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
        var assignedValueStream = moveStreamFactory.forEach(variableMetaModel.type(), false)
                .filter((solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList)
                .map((solutionView, value) -> new FullElementPosition<>(value,
                        solutionView.getPositionOf(variableMetaModel, value).ensureAssigned(), planningIdGetter));
        if (planningIdGetter == null) { // If the user hasn't defined a planning ID, we will follow a slower path.
            return moveStreamFactory.pick(assignedValueStream)
                    .pick(assignedValueStream,
                            EnumeratingJoiners.filtering(this::isValidSwap))
                    .asMove(this::buildMove);
        } else {
            return moveStreamFactory.pick(assignedValueStream)
                    .pick(assignedValueStream,
                            EnumeratingJoiners.lessThan(a -> a),
                            EnumeratingJoiners.filtering(this::isValidSwap))
                    .asMove(this::buildMove);
        }
    }

    private Move<Solution_> buildMove(SolutionView<Solution_> solutionView, FullElementPosition<Entity_, Value_> a,
            FullElementPosition<Entity_, Value_> b) {
        return Moves.swap(variableMetaModel, a.elementPosition, b.elementPosition);
    }

    private boolean isValidSwap(SolutionView<Solution_> solutionView, FullElementPosition<Entity_, Value_> leftPosition,
            FullElementPosition<Entity_, Value_> rightPosition) {
        if (Objects.equals(leftPosition, rightPosition)) {
            return false;
        }
        return solutionView.isValueInRange(variableMetaModel, rightPosition.entity(), leftPosition.value())
                && solutionView.isValueInRange(variableMetaModel, leftPosition.entity(), rightPosition.value());
    }

    @NullMarked
    private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition,
            @Nullable Function<Entity_, Comparable> planningIdGetter)
            implements
                Comparable<FullElementPosition<Entity_, Value_>> {

        public Entity_ entity() {
            return elementPosition.entity();
        }

        public int index() {
            return elementPosition.index();
        }

        @Override
        public int compareTo(FullElementPosition<Entity_, Value_> o) {
            if (planningIdGetter == null) { // The code will not get here if the getter is null.
                throw new IllegalStateException("Impossible state: The planningIdGetter is null, cannot compare entities.");
            }
            var entityComparison = planningIdGetter.apply(this.entity()).compareTo(planningIdGetter.apply(o.entity()));
            if (entityComparison != 0) {
                return entityComparison;
            }
            return Integer.compare(this.index(), o.index());
        }

        @Override
        public String toString() {
            return value + "@" + elementPosition;
        }
    }

}
